package com.eedi.framework.oauth2.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.eedi.framework.common.enums.UserTypeEnum;
import com.eedi.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.eedi.framework.common.pojo.PageResult;
import com.eedi.framework.common.util.date.DateUtils;
import com.eedi.framework.oauth2.controller.sys.vo.token.SysOAuth2AccessTokenPageReq;
import com.eedi.framework.oauth2.dal.SysOAuth2AccessTokenRedisDAO;
import com.eedi.framework.oauth2.dal.dataobject.SysOAuth2AccessTokenDO;
import com.eedi.framework.oauth2.dal.dataobject.SysOAuth2ClientDO;
import com.eedi.framework.oauth2.dal.dataobject.SysOAuth2RefreshTokenDO;
import com.eedi.framework.oauth2.dal.mapper.SysOAuth2AccessTokenMapper;
import com.eedi.framework.oauth2.dal.mapper.SysOAuth2RefreshTokenMapper;
import com.eedi.framework.oauth2.enums.SysOauth2ErrorCodeConstants;
import com.eedi.framework.tenant.core.context.TenantContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;

import static com.eedi.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static com.eedi.framework.common.util.collection.CollectionUtils.convertSet;

/**
 * OAuth2.0 Token Service 实现类
 *
 * @author 永聚长青源码
 */
@Service
public class SysOAuth2TokenServiceImpl implements SysOAuth2TokenService {

    @Resource
    private SysOAuth2AccessTokenMapper oauth2AccessTokenMapperSys;
    @Resource
    private SysOAuth2RefreshTokenMapper oauth2RefreshTokenMapperSys;

    @Resource
    private SysOAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAOSys;

    @Resource
    private SysOAuth2ClientService oauth2ClientServiceSys;

    @Override
    @Transactional
    public SysOAuth2AccessTokenDO createAccessToken(String userAccountId, UserTypeEnum userType, String clientCode, List<String> scopes) {
        SysOAuth2ClientDO clientDO = oauth2ClientServiceSys.validOAuthClientFromCache(clientCode);
        // 查询当前有效token
        List<SysOAuth2AccessTokenDO> existTokenList = oauth2AccessTokenMapperSys.selectByUserId(userAccountId, userType);
        // 创建刷新令牌
        SysOAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userAccountId, userType, clientDO, scopes);
        // 创建访问令牌
        SysOAuth2AccessTokenDO sysOAuth2AccessTokenDO = createOAuth2AccessToken(refreshTokenDO, clientDO);
        // 互踢 移除同一用户同一端所有未过期token
        if (CollUtil.isNotEmpty(existTokenList)) {
            existTokenList.forEach(token -> {
                        oauth2AccessTokenMapperSys.deleteById(token.getSysOauth2AccessTokenId());
                        oauth2AccessTokenRedisDAOSys.delete(token.getAccessToken());
                        // 删除刷新令牌
                        oauth2RefreshTokenMapperSys.deleteByRefreshToken(token.getRefreshToken());
                    }
            );
        }
        return sysOAuth2AccessTokenDO;
    }

    @Override
    public SysOAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
        // 查询访问令牌
        SysOAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapperSys.selectByRefreshToken(refreshToken);
        if (refreshTokenDO == null) {
            throw exception0(SysOauth2ErrorCodeConstants.SYS_OAUTH2_REFRESH_TOKEN_ERROR.getCode(), "无效的刷新令牌");
        }

        // 校验 Client 匹配
        SysOAuth2ClientDO clientDO = oauth2ClientServiceSys.validOAuthClientFromCache(clientId);
        if (ObjectUtil.notEqual(clientDO.getSysOauth2ClientId(), refreshTokenDO.getSysOauth2ClientId())) {
            throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确");
        }

        // 移除相关的访问令牌
        List<SysOAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapperSys.selectListByRefreshToken(refreshToken);
        if (CollUtil.isNotEmpty(accessTokenDOs)) {
            oauth2AccessTokenMapperSys.deleteBatchIds(convertSet(accessTokenDOs, SysOAuth2AccessTokenDO::getSysOauth2AccessTokenId));
            oauth2AccessTokenRedisDAOSys.deleteList(convertSet(accessTokenDOs, SysOAuth2AccessTokenDO::getAccessToken));
        }

        oauth2RefreshTokenMapperSys.deleteById(refreshTokenDO.getSysOauth2RefreshTokenId());
        // 已过期的情况下，删除刷新令牌
        if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) {
            throw exception0(SysOauth2ErrorCodeConstants.SYS_OAUTH2_REFRESH_TOKEN_ERROR.getCode(), "刷新令牌已过期");
        }
        //创建新的刷新令牌，避免重复使用
        SysOAuth2RefreshTokenDO oAuth2RefreshToken = createOAuth2RefreshToken(refreshTokenDO.getSysUserId(), refreshTokenDO.getUserType(), clientDO, null);

        // 创建访问令牌
        return createOAuth2AccessToken(oAuth2RefreshToken, clientDO);
    }

    private SysOAuth2RefreshTokenDO createOAuth2RefreshToken(String userId, UserTypeEnum userType, SysOAuth2ClientDO clientDO, List<String> scopes) {
        SysOAuth2RefreshTokenDO refreshToken = new SysOAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
                .setSysUserId(userId).setUserType(userType)
                .setSysOauth2ClientId(clientDO.getSysOauth2ClientId()).setScopes(scopes)
                .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds()));
        oauth2RefreshTokenMapperSys.insert(refreshToken);
        return refreshToken;
    }

    @Override
    public SysOAuth2AccessTokenDO getAccessToken(String accessToken) {
        // 优先从 Redis 中获取
        SysOAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAOSys.get(accessToken);
        if (accessTokenDO != null) {
            return accessTokenDO;
        }

        // 获取不到，从 MySQL 中获取
        accessTokenDO = oauth2AccessTokenMapperSys.selectByAccessToken(accessToken);
        // 如果在 MySQL 存在，则往 Redis 中写入
        if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
            oauth2AccessTokenRedisDAOSys.set(accessTokenDO);
        }
        return accessTokenDO;
    }

    @Override
    public SysOAuth2AccessTokenDO checkAccessToken(String accessToken) {
        SysOAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken);
        if (accessTokenDO == null) {
            throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在");
        }
        if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
            throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期");
        }
        return accessTokenDO;
    }

    @Override
    public SysOAuth2AccessTokenDO removeAccessToken(String accessToken) {
        // 删除访问令牌
        SysOAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapperSys.selectByAccessToken(accessToken);
        if (accessTokenDO == null) {
            return null;
        }
        oauth2AccessTokenMapperSys.deleteById(accessTokenDO.getSysOauth2AccessTokenId());
        oauth2AccessTokenRedisDAOSys.delete(accessToken);
        // 删除刷新令牌
        oauth2RefreshTokenMapperSys.deleteByRefreshToken(accessTokenDO.getRefreshToken());
        return accessTokenDO;
    }

    @Override
    public PageResult<SysOAuth2AccessTokenDO> getAccessTokenPage(SysOAuth2AccessTokenPageReq reqVO) {
        return oauth2AccessTokenMapperSys.selectPage(reqVO);
    }

    @Override
    public SysOAuth2AccessTokenDO selectAccessTokenByUserId(String userId, UserTypeEnum userType) {
        List<SysOAuth2AccessTokenDO> oAuth2AccessTokenList = oauth2AccessTokenMapperSys.selectByUserId(userId,userType);
        return CollUtil.isNotEmpty(oAuth2AccessTokenList) ? oAuth2AccessTokenList.get(0) : null;
    }

    private SysOAuth2AccessTokenDO createOAuth2AccessToken(SysOAuth2RefreshTokenDO refreshTokenDO, SysOAuth2ClientDO clientDO) {
        SysOAuth2AccessTokenDO accessTokenDO = new SysOAuth2AccessTokenDO().setAccessToken(generateAccessToken())
                .setSysUserId(refreshTokenDO.getSysUserId()).setSysUserType(refreshTokenDO.getUserType())
                .setSysOauth2ClientId(clientDO.getSysOauth2ClientId()).setScopes(refreshTokenDO.getScopes())
                .setRefreshToken(refreshTokenDO.getRefreshToken())
                .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));
        // 手动设置租户编号，避免缓存到 Redis 的时候，无对应的租户编号
        accessTokenDO.setSysTenantId(TenantContextHolder.getTenantId());
        oauth2AccessTokenMapperSys.insert(accessTokenDO);
        // 记录到 Redis 中
        oauth2AccessTokenRedisDAOSys.set(accessTokenDO);
        return accessTokenDO;
    }



    private static String generateAccessToken() {
        return IdUtil.fastSimpleUUID();
    }

    private static String generateRefreshToken() {
        return IdUtil.fastSimpleUUID();
    }

}
